#pragma once
#include <algorithm> // std::find_if()
#include <cassert> // assert()
#include <functional> // std::function
#include <iterator> // std::back_inserter
#include <memory> // std::shared_ptr, std::weak_ptr
#include <mutex> // std::mutex, std::lock_guard
#include <thread> // std::this_thread::yield()
#include <type_traits> // std::is_same
#include <vector> // std::vector

namespace Zep
{
// implementational details
namespace detail
{
/// Interface for type erasure when disconnecting slots
struct disconnector
{
    virtual void operator()(std::size_t index) const = 0;
};
/// Deleter that doesn't delete
inline void no_delete(disconnector*){};
} // namespace detail

/// Base template for the signal class
template <class P, class T>
class signal_type;

/// Connection class.
///
/// This is used to be able to disconnect slots after they have been connected.
/// Used as return type for the connect method of the signals.
///
/// Connections are default constructible.
/// Connections are not copy constructible or copy assignable.
/// Connections are move constructible and move assignable.
///
class connection
{
public:
    /// Default constructor
    connection()
        : _index()
    {
    }

    // Connection are not copy constructible or copy assignable
    connection(connection const&) = delete;
    connection& operator=(connection const&) = delete;

    /// Move constructor
    /// @param other   The instance to move from.
    connection(connection&& other)
        : _weak_disconnector(std::move(other._weak_disconnector))
        , _index(other._index)
    {
    }

    /// Move assign operator.
    /// @param other   The instance to move from.
    connection& operator=(connection&& other)
    {
        _weak_disconnector = std::move(other._weak_disconnector);
        _index = other._index;
        return *this;
    }

    /// @returns `true` if the connection is connected to a signal object,
    ///          and `false` otherwise.
    bool connected() const
    {
        return !_weak_disconnector.expired();
    }

    /// Disconnect the slot from the connection.
    ///
    /// If the connection represents a slot that is connected to a signal object, calling
    /// this method will disconnect the slot from that object. The result of this operation
    /// is that the slot will stop receiving calls when the signal is invoked.
    void disconnect();

private:
    /// The signal template is a friend of the connection, since it is the
    /// only one allowed to create instances using the meaningful constructor.
    template <class P, class T>
    friend class signal_type;

    /// Create a connection.
    /// @param shared_disconnector   Disconnector instance that will be used to disconnect
    ///                              the connection when the time comes. A weak pointer
    ///                              to the disconnector will be held within the connection
    ///                              object.
    /// @param index                 The slot index of the connection.
    connection(std::shared_ptr<detail::disconnector> const& shared_disconnector, std::size_t index)
        : _weak_disconnector(shared_disconnector)
        , _index(index)
    {
    }

    /// Weak pointer to the current disconnector functor.
    std::weak_ptr<detail::disconnector> _weak_disconnector;
    /// Slot index of the connected slot.
    std::size_t _index;
};

/// Scoped connection class.
///
/// This type of connection is automatically disconnected when
/// the connection object is destructed.
///
class scoped_connection
{
public:
    /// Scoped are default constructible
    scoped_connection() = default;
    /// Scoped connections are not copy constructible
    scoped_connection(scoped_connection const&) = delete;
    /// Scoped connections are not copy assingable
    scoped_connection& operator=(scoped_connection const&) = delete;

    /// Move constructor
    scoped_connection(scoped_connection&& other)
        : _connection(std::move(other._connection))
    {
    }

    /// Move assign operator.
    /// @param other   The instance to move from.
    scoped_connection& operator=(scoped_connection&& other)
    {
        reset(std::move(other._connection));
        return *this;
    }

    /// Construct a scoped connection from a connection object
    /// @param connection   The connection object to manage
    scoped_connection(connection&& c)
        : _connection(std::forward<connection>(c))
    {
    }

    /// destructor
    ~scoped_connection()
    {
        disconnect();
    }

    /// Assignment operator moving a new connection into the instance.
    /// @note If the scoped_connection instance already contains a
    ///       connection, that connection will be disconnected as if
    ///       the scoped_connection was destroyed.
    /// @param c   New connection to manage
    scoped_connection& operator=(connection&& c)
    {
        reset(std::forward<connection>(c));
        return *this;
    }

    /// Reset the underlying connection to another connection.
    /// @note The connection currently managed by the scoped_connection
    ///       instance will be disconnected when resetting.
    /// @param c   New connection to manage
    void reset(connection&& c = {})
    {
        disconnect();
        _connection = std::move(c);
    }

    /// Release the underlying connection, without disconnecting it.
    /// @returns The newly released connection instance is returned.
    connection release()
    {
        connection c = std::move(_connection);
        _connection = connection{};
        return c;
    }

    ///
    /// @returns `true` if the connection is connected to a signal object,
    ///          and `false` otherwise.
    bool connected() const
    {
        return _connection.connected();
    }

    /// Disconnect the slot from the connection.
    ///
    /// If the connection represents a slot that is connected to a signal object, calling
    /// this method will disconnect the slot from that object. The result of this operation
    /// is that the slot will stop receiving calls when the signal is invoked.
    void disconnect()
    {
        _connection.disconnect();
    }

private:
    /// Underlying connection object
    connection _connection;
};

/// Policy for multi threaded use of signals.
///
/// This policy provides mutex and lock types for use in
/// a multithreaded environment, where signals and slots
/// may exists in different threads.
///
/// This policy is used in the `nod::signal` type provided
/// by the library.
struct multithread_policy
{
    using mutex_type = std::mutex;
    using mutex_lock_type = std::unique_lock<mutex_type>;
    /// Function that yields the current thread, allowing
    /// the OS to reschedule.
    static void yield_thread()
    {
        std::this_thread::yield();
    }
    /// Function that defers a lock to a lock function that prevents deadlock
    static mutex_lock_type defer_lock(mutex_type& m)
    {
        return mutex_lock_type{ m, std::defer_lock };
    }
    /// Function that locks two mutexes and prevents deadlock
    static void lock(mutex_lock_type& a, mutex_lock_type& b)
    {
        std::lock(a, b);
    }
};

/// Policy for single threaded use of signals.
///
/// This policy provides dummy implementations for mutex
/// and lock types, resulting in that no synchronization
/// will take place.
///
/// This policy is used in the `nod::unsafe_signal` type
/// provided by the library.
struct singlethread_policy
{
    /// Dummy mutex type that doesn't do anything
    struct mutex_type
    {
    };
    /// Dummy lock type, that doesn't do any locking.
    struct mutex_lock_type
    {
        /// A lock type must be constructible from a
        /// mutex type from the same thread policy.
        explicit mutex_lock_type(mutex_type const&)
        {
        }
    };
    /// Dummy implementation of thread yielding, that
    /// doesn't do any actual yielding.
    static void yield_thread()
    {
    }
    /// Dummy implemention of defer_lock that doesn't
    /// do anything
    static mutex_lock_type defer_lock(mutex_type& m)
    {
        return mutex_lock_type{ m };
    }
    /// Dummy implemention of lock that doesn't
    /// do anything
    static void lock(mutex_lock_type&, mutex_lock_type&)
    {
    }
};

/// Signal accumulator class template.
///
/// This acts sort of as a proxy for triggering a signal and
/// accumulating the slot return values.
///
/// This class is not really intended to instantiate by client code.
/// Instances are aquired as return values of the method `accumulate()`
/// called on signals.
///
/// @tparam S      Type of signal. The signal_accumulator acts
///                as a type of proxy for a signal instance of
///                this type.
/// @tparam T      Type of initial value of the accumulate algorithm.
///                This type must meet the requirements of `CopyAssignable`
///                and `CopyConstructible`
/// @tparam F      Type of accumulation function.
/// @tparam A...   Argument types of the underlying signal type.
///
template <class S, class T, class F, class... A>
class signal_accumulator
{
public:
    /// Result type when calling the accumulating function operator.
    #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
    using result_type = typename std::invoke_result<F, T, typename S::slot_type::result_type>::type;
    #else
    using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::type;
    #endif

    /// Construct a signal_accumulator as a proxy to a given signal
    //
    /// @param signal   Signal instance.
    /// @param init     Initial value of the accumulate algorithm.
    /// @param func     Binary operation function object that will be
    ///                 applied to all slot return values.
    ///                 The signature of the function should be
    ///                 equivalent of the following:
    ///                   `R func( T1 const& a, T2 const& b )`
    ///                  - The signature does not need to have `const&`.
    ///                  - The initial value, type `T`, must be implicitly
    ///                    convertible to `R`
    ///                  - The return type `R` must be implicitly convertible
    ///                    to type `T1`.
    ///                  - The type `R` must be `CopyAssignable`.
    ///                  - The type `S::slot_type::result_type` (return type of
    ///                    the signals slots) must be implicitly convertible to
    ///                    type `T2`.
    signal_accumulator(S const& signal, T init, F func)
        : _signal(signal)
        , _init(init)
        , _func(func)
    {
    }

    /// Function call operator.
    ///
    /// Calling this will trigger the underlying signal and accumulate
    /// all of the connected slots return values with the current
    /// initial value and accumulator function.
    ///
    /// When called, this will invoke the accumulator function will
    /// be called for each return value of the slots. The semantics
    /// are similar to the `std::accumulate` algorithm.
    ///
    /// @param args   Arguments to propagate to the slots of the
    ///               underlying when triggering the signal.
    result_type operator()(A const&... args) const
    {
        return _signal.trigger_with_accumulator(_init, _func, args...);
    }

private:
    /// Reference to the underlying signal to proxy.
    S const& _signal;
    /// Initial value of the accumulate algorithm.
    T _init;
    /// Accumulator function.
    F _func;
};

/// Signal template specialization.
///
/// This is the main signal implementation, and it is used to
/// implement the observer pattern whithout the overhead
/// boilerplate code that typically comes with it.
///
/// Any function or function object is considered a slot, and
/// can be connected to a signal instance, as long as the signature
/// of the slot matches the signature of the signal.
///
/// @tparam P      Threading policy for the signal.
///                A threading policy must provide two type definitions:
///                 - P::mutex_type, this type will be used as a mutex
///                   in the signal_type class template.
///                 - P::mutex_lock_type, this type must implement a
///                   constructor that takes a P::mutex_type as a parameter,
///                   and it must have the semantics of a scoped mutex lock
///                   like std::lock_guard, i.e. locking in the constructor
///                   and unlocking in the destructor.
///
/// @tparam R      Return value type of the slots connected to the signal.
/// @tparam A...   Argument types of the slots connected to the signal.
template <class P, class R, class... A>
class signal_type<P, R(A...)>
{
public:
    /// signals are not copy constructible
    signal_type(signal_type const&) = delete;
    /// signals are not copy assignable
    signal_type& operator=(signal_type const&) = delete;
    /// signals are move constructible
    signal_type(signal_type&& other)
    {
        mutex_lock_type lock{ other._mutex };
        _slot_count = std::move(other._slot_count);
        _slots = std::move(other._slots);
        if (other._shared_disconnector != nullptr)
        {
            _disconnector = disconnector{ this };
            _shared_disconnector = std::move(other._shared_disconnector);
            // replace the disconnector with our own disconnector
            *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
        }
    }
    /// signals are move assignable
    signal_type& operator=(signal_type&& other)
    {
        auto lock = thread_policy::defer_lock(_mutex);
        auto other_lock = thread_policy::defer_lock(other._mutex);
        thread_policy::lock(lock, other_lock);

        _slot_count = std::move(other._slot_count);
        _slots = std::move(other._slots);
        if (other._shared_disconnector != nullptr)
        {
            _disconnector = disconnector{ this };
            _shared_disconnector = std::move(other._shared_disconnector);
            // replace the disconnector with our own disconnector
            *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
        }
        return *this;
    }

    /// signals are default constructible
    signal_type()
        : _slot_count(0)
    {
    }

    // Destruct the signal object.
    ~signal_type()
    {
        invalidate_disconnector();
    }

    /// Type that will be used to store the slots for this signal type.
    using slot_type = std::function<R(A...)>;
    /// Type that is used for counting the slots connected to this signal.
    using size_type = typename std::vector<slot_type>::size_type;

    /// Connect a new slot to the signal.
    ///
    /// The connected slot will be called every time the signal
    /// is triggered.
    /// @param slot   The slot to connect. This must be a callable with
    ///               the same signature as the signal itself.
    /// @return       A connection object is returned, and can be used to
    ///               disconnect the slot.
    template <class T>
    connection connect(T&& slot)
    {
        mutex_lock_type lock{ _mutex };
        _slots.push_back(std::forward<T>(slot));
        std::size_t index = _slots.size() - 1;
        if (_shared_disconnector == nullptr)
        {
            _disconnector = disconnector{ this };
            _shared_disconnector = std::shared_ptr<detail::disconnector>{ &_disconnector, detail::no_delete };
        }
        ++_slot_count;
        return connection{ _shared_disconnector, index };
    }

    /// Function call operator.
    ///
    /// Calling this is how the signal is triggered and the
    /// connected slots are called.
    ///
    /// @note The slots will be called in the order they were
    ///       connected to the signal.
    ///
    /// @param args   Arguments that will be propagated to the
    ///               connected slots when they are called.
    void operator()(A const&... args) const
    {
        for (auto const& slot : copy_slots())
        {
            if (slot)
            {
                slot(args...);
            }
        }
    }

    /// Construct a accumulator proxy object for the signal.
    ///
    /// The intended purpose of this function is to create a function
    /// object that can be used to trigger the signal and accumulate
    /// all the slot return values.
    ///
    /// The algorithm used to accumulate slot return values is similar
    /// to `std::accumulate`. A given binary function is called for
    /// each return value with the parameters consisting of the
    /// return value of the accumulator function applied to the
    /// previous slots return value, and the current slots return value.
    /// A initial value must be provided for the first slot return type.
    ///
    /// @note This can only be used on signals that have slots with
    ///       non-void return types, since we can't accumulate void
    ///       values.
    ///
    /// @tparam T      The type of the initial value given to the accumulator.
    /// @tparam F      The accumulator function type.
    /// @param init    Initial value given to the accumulator.
    /// @param op      Binary operator function object to apply by the accumulator.
    ///                The signature of the function should be
    ///                equivalent of the following:
    ///                  `R func( T1 const& a, T2 const& b )`
    ///                 - The signature does not need to have `const&`.
    ///                 - The initial value, type `T`, must be implicitly
    ///                   convertible to `R`
    ///                 - The return type `R` must be implicitly convertible
    ///                   to type `T1`.
    ///                 - The type `R` must be `CopyAssignable`.
    ///                 - The type `S::slot_type::result_type` (return type of
    ///                   the signals slots) must be implicitly convertible to
    ///                   type `T2`.
    template <class T, class F>
    signal_accumulator<signal_type, T, F, A...> accumulate(T init, F op) const
    {
        static_assert(std::is_same<R, void>::value == false, "Unable to accumulate slot return values with 'void' as return type.");
        return { *this, init, op };
    }

    /// Trigger the signal, calling the slots and aggregate all
    /// the slot return values into a container.
    ///
    /// @tparam C     The type of container. This type must be
    ///               `DefaultConstructible`, and usable with
    ///               `std::back_insert_iterator`. Additionally it
    ///               must be either copyable or moveable.
    /// @param args   The arguments to propagate to the slots.
    template <class C>
    C aggregate(A const&... args) const
    {
        static_assert(std::is_same<R, void>::value == false, "Unable to aggregate slot return values with 'void' as return type.");
        C container;
        auto iterator = std::back_inserter(container);
        for (auto const& slot : copy_slots())
        {
            if (slot)
            {
                (*iterator) = slot(args...);
            }
        }
        return container;
    }

    /// Count the number of slots connected to this signal
    /// @returns   The number of connected slots
    size_type slot_count() const
    {
        return _slot_count;
    }

    /// Determine if the signal is empty, i.e. no slots are connected
    /// to it.
    /// @returns   `true` is returned if the signal has no connected
    ///            slots, and `false` otherwise.
    bool empty() const
    {
        return slot_count() == 0;
    }

    /// Disconnects all slots
    /// @note This operation invalidates all scoped_connection objects
    void disconnect_all_slots()
    {
        mutex_lock_type lock{ _mutex };
        _slots.clear();
        _slot_count = 0;
        invalidate_disconnector();
    }

private:
    template <class, class, class, class...>
    friend class signal_accumulator;
    /// Thread policy currently in use
    using thread_policy = P;
    /// Type of mutex, provided by threading policy
    using mutex_type = typename thread_policy::mutex_type;
    /// Type of mutex lock, provided by threading policy
    using mutex_lock_type = typename thread_policy::mutex_lock_type;

    /// Invalidate the internal disconnector object in a way
    /// that is safe according to the current thread policy.
    ///
    /// This will effectively make all current connection objects to
    /// to this signal incapable of disconnecting, since they keep a
    /// weak pointer to the shared disconnector object.
    void invalidate_disconnector()
    {
        // If we are unlucky, some of the connected slots
        // might be in the process of disconnecting from other threads.
        // If this happens, we are risking to destruct the disconnector
        // object managed by our shared pointer before they are done
        // disconnecting. This would be bad. To solve this problem, we
        // discard the shared pointer (that is pointing to the disconnector
        // object within our own instance), but keep a weak pointer to that
        // instance. We then stall the destruction until all other weak
        // pointers have released their "lock" (indicated by the fact that
        // we will get a nullptr when locking our weak pointer).
        std::weak_ptr<detail::disconnector> weak{ _shared_disconnector };
        _shared_disconnector.reset();
        while (weak.lock() != nullptr)
        {
            // we just yield here, allowing the OS to reschedule. We do
            // this until all threads has released the disconnector object.
            thread_policy::yield_thread();
        }
    }

    /// Retrieve a copy of the current slots
    ///
    /// It's useful and necessary to copy the slots so we don't need
    /// to hold the lock while calling the slots. If we hold the lock
    /// we prevent the called slots from modifying the slots vector.
    /// This simple "double buffering" will allow slots to disconnect
    /// themself or other slots and connect new slots.
    std::vector<slot_type> copy_slots() const
    {
        mutex_lock_type lock{ _mutex };
        return _slots;
    }

    /// Implementation of the signal accumulator function call
    template <class T, class F>
    typename signal_accumulator<signal_type, T, F, A...>::result_type trigger_with_accumulator(T value, F& func, A const&... args) const
    {
        for (auto const& slot : copy_slots())
        {
            if (slot)
            {
                value = func(value, slot(args...));
            }
        }
        return value;
    }

    /// Implementation of the disconnection operation.
    ///
    /// This is private, and only called by the connection
    /// objects created when connecting slots to this signal.
    /// @param index   The slot index of the slot that should
    ///                be disconnected.
    void disconnect(std::size_t index)
    {
        mutex_lock_type lock(_mutex);
        assert(_slots.size() > index);
        if (_slots[index] != nullptr)
        {
            --_slot_count;
        }
        _slots[index] = slot_type{};
        while (_slots.size() > 0 && !_slots.back())
        {
            _slots.pop_back();
        }
    }

    /// Implementation of the shared disconnection state
    /// used by all connection created by signal instances.
    ///
    /// This inherits the @ref detail::disconnector interface
    /// for type erasure.
    struct disconnector : detail::disconnector
    {
        /// Default constructor, resulting in a no-op disconnector.
        disconnector()
            : _ptr(nullptr)
        {
        }

        /// Create a disconnector that works with a given signal instance.
        /// @param ptr   Pointer to the signal instance that the disconnector
        ///              should work with.
        disconnector(signal_type<P, R(A...)>* ptr)
            : _ptr(ptr)
        {
        }

        /// Disconnect a given slot on the current signal instance.
        /// @note If the instance is default constructed, or created
        ///       with `nullptr` as signal pointer this operation will
        ///       effectively be a no-op.
        /// @param index   The index of the slot to disconnect.
        void operator()(std::size_t index) const override
        {
            if (_ptr)
            {
                _ptr->disconnect(index);
            }
        }

        /// Pointer to the current signal.
        signal_type<P, R(A...)>* _ptr;
    };

    /// Mutex to synchronize access to the slot vector
    mutable mutex_type _mutex;
    /// Vector of all connected slots
    std::vector<slot_type> _slots;
    /// Number of connected slots
    size_type _slot_count;
    /// Disconnector operation, used for executing disconnection in a
    /// type erased manner.
    disconnector _disconnector;
    /// Shared pointer to the disconnector. All connection objects has a
    /// weak pointer to this pointer for performing disconnections.
    std::shared_ptr<detail::disconnector> _shared_disconnector;
};

// Implementation of the disconnect operation of the connection class
inline void connection::disconnect()
{
    auto ptr = _weak_disconnector.lock();
    if (ptr)
    {
        (*ptr)(_index);
    }
    _weak_disconnector.reset();
}

/// Signal type that is safe to use in multithreaded environments,
/// where the signal and slots exists in different threads.
/// The multithreaded policy provides mutexes and locks to synchronize
/// access to the signals internals.
///
/// This is the recommended signal type, even for single threaded
/// environments.
template <class T>
using signal = signal_type<multithread_policy, T>;

/// Signal type that is unsafe in multithreaded environments.
/// No synchronizations are provided to the signal_type for accessing
/// the internals.
///
/// Only use this signal type if you are sure that your environment is
/// single threaded and performance is of importance.
template <class T>
using unsafe_signal = signal_type<singlethread_policy, T>;
} // namespace Zep

